﻿// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Threading.Tasks;
using System.Threading;

namespace SnapObjects.Data
{
    internal abstract class ColumnSchemaProvider
    {
        protected readonly DataContext _dataContext;
        protected readonly IList<string> _notInWhereType;
        public ColumnSchemaProvider(DataContext dataContext)
        {
            _dataContext = dataContext;
            _notInWhereType = this.GetNotInWhereType();
        }

        public virtual IReadOnlyList<IAdoDbParameter> GetSqlParameterSchema(string sqlText)
        {
            return new List<IAdoDbParameter>();
        }

        public virtual async Task<IReadOnlyList<IAdoDbParameter>> GetSqlParameterSchemaAsync(
            string sqlText, CancellationToken cancellationToken)
        {
            return new List<IAdoDbParameter>();
        }

        protected virtual CommandBehavior GetCommandBehavior()
        {
            return CommandBehavior.KeyInfo | CommandBehavior.SchemaOnly;
        }

        protected List<List<DbColumnInfo>> GetDbColumnInfo(IDataReader reader)
        {
            var columnSchemas = new List<List<DbColumnInfo>>();

            void CreateDbColumnInfo()
            {
                var columnSchema = this.CreateDbColumnInfo(reader);

                if (columnSchema.Count > 0)
                {
                    columnSchemas.Add(columnSchema);
                }
            }

            CreateDbColumnInfo();

            while (reader.NextResult())
            {
                CreateDbColumnInfo();
            }

            return columnSchemas;
        }

        protected virtual List<DbColumnInfo> CreateDbColumnInfo(IDataReader reader)
        {
            var columnSchema = new List<DbColumnInfo>();

            var schemaTable = reader.GetSchemaTable();

            for (var i = 0; i < schemaTable?.Rows.Count; i++)
            {
                var columnName = this.ColumnName(schemaTable.Rows[i], reader);
                var table = this.ColumnTableName(schemaTable.Rows[i], reader);
                var schema = this.ColumnSchemaName(schemaTable.Rows[i], reader);
                var ordinal = this.ColumnOrdinal(schemaTable.Rows[i], reader);
                var dataType = this.ColumnDataType(schemaTable.Rows[i], reader);
                var isKey = this.IsKey(schemaTable.Rows[i], reader);
                var nullable = this.AllowDbNull(schemaTable.Rows[i], reader);
                var isExpression = this.IsExpression(schemaTable.Rows[i], reader);
                var isIdentity = this.IsIdentity(schemaTable.Rows[i], reader);
                var isHidden = this.IsHidden(schemaTable.Rows[i], reader);
                var isTimestamp = this.IsTimestamp(schemaTable.Rows[i], reader);
                var dataTypeName = this.ColumnDataTypeName(schemaTable.Rows[i], reader);
                var columnSize = this.ColumnSize(schemaTable.Rows[i], reader);
                var baseColumnName = this.BaseColumnName(schemaTable.Rows[i], reader);
                var isReadOnly = this.IsReadOnly(schemaTable.Rows[i], reader);
                var isAliased = this.IsAliased(schemaTable.Rows[i], reader);
                var numPrecsion = this.NumericPrecision(schemaTable.Rows[i], reader);
                var numScale = this.NumericScale(schemaTable.Rows[i], reader);
                var isUnique = this.IsUnique(schemaTable.Rows[i], reader);
                var isNotInWhere = this.IsNotInWhere(schemaTable.Rows[i], reader);

                if (!isHidden)
                {
                    var columnInfo = new DbColumnInfo(
                        ordinal,
                        columnName,
                        dataType,
                        isKey,
                        isIdentity,
                        nullable,
                        isExpression,
                        schema,
                        table,
                        isTimestamp,
                        dataTypeName,
                        columnSize,
                        baseColumnName,
                        isReadOnly,
                        isAliased,
                        numPrecsion,
                        numScale,
                        isUnique);

                    columnInfo.IsNotInWhere = isNotInWhere;

                    columnSchema.Add(columnInfo);
                }
            }

            return columnSchema;
        }

        protected virtual string ColumnName(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["columnname"];

            var value = row.ItemArray[column.Ordinal];

            var name = this.GetSchemaItemValue<string>(value);

            if (!String.IsNullOrWhiteSpace(name))
            {
                if (name.Any(m => !Char.IsLetterOrDigit(m) && !'_'.Equals(m)))
                {
                    name = String.Empty;
                }
            }

            if (String.IsNullOrWhiteSpace(name))
            {
                name = "Compute" + this.ColumnOrdinal(row, reader);
            }

            return name;
        }

        protected virtual int ColumnOrdinal(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["columnordinal"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<int>(value);
        }

        protected virtual string ColumnSchemaName(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["baseschemaname"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<string>(value);
        }

        protected virtual string ColumnTableName(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["basetablename"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<string>(value);
        }

        protected virtual Type ColumnDataType(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["datatype"];

            var value = row.ItemArray[column.Ordinal];

            if (value == DBNull.Value)
            {
                var oridnal = this.ColumnOrdinal(row, reader);

                value = reader.GetFieldType(oridnal);
            }

            return this.GetSchemaItemValue<Type>(value);
        }

        protected virtual bool IsKey(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["iskey"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual bool AllowDbNull(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["allowdbnull"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual bool IsIdentity(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["isidentity"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual bool IsExpression(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["isexpression"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual bool IsHidden(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["ishidden"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual string ColumnDataTypeName(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["datatypename"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<string>(value);
        }

        protected virtual int ColumnSize(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["columnsize"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<int>(value);
        }

        protected virtual bool IsTimestamp(DataRow row, IDataReader reader)
        {
            return false;
        }

        protected virtual string BaseColumnName(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["basecolumnname"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<string>(value);
        }

        protected virtual bool IsReadOnly(DataRow row, IDataReader reader)
        {
            return false;
        }

        protected virtual bool IsAliased(DataRow row, IDataReader reader)
        {
            return false;
        }

        protected virtual bool IsUnique(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["isunique"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<bool>(value);
        }

        protected virtual int NumericPrecision(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["numericprecision"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<int>(value);
        }

        protected virtual int NumericScale(DataRow row, IDataReader reader)
        {
            var column = row.Table.Columns["numericscale"];

            var value = row.ItemArray[column.Ordinal];

            return this.GetSchemaItemValue<int>(value);
        }

        protected virtual bool IsNotInWhere(DataRow row, IDataReader reader)
        {
            var dataType = this.ColumnDataType(row, reader);

            if (dataType.IsPrimitive || dataType == typeof(decimal))
            {
                return false;
            }

            if (dataType == typeof(byte[]))
            {
                return true;
            }

            var typeName = this.ColumnDataTypeName(row, reader);

            return _notInWhereType.Contains(typeName.ToLower());
        }

        protected virtual IList<string> GetNotInWhereType()
        {
            return new List<string>();
        }

        protected TValue GetSchemaItemValue<TValue>(object value)
        {
            if (value.GetType() != typeof(DBNull))
            {
                if (typeof(TValue) == typeof(Type))
                {
                    return (TValue)value;
                }

                return ValueConvert.Convert<TValue>(value);
            }

            return default;
        }

        protected IList<IAdoDbParameter> CloneAdoDbParameter(IEnumerable<IAdoDbParameter> parameters)
        {
            var list = new List<IAdoDbParameter>();

            foreach (var parameter in parameters)
            {
                if (parameter is AdoDbParameter param)
                {
                    var p = new AdoDbParameter(
                        param.ParameterName,
                        param.Value,
                        param.ClrType,
                        param.Direction,
                        param.TypeName,
                        param.Precision,
                        param.Scale,
                        param.Position,
                        param.Size,
                        param.ArrayBindSize);

                    list.Add(p);
                }
            }

            return list;
        }
    }
}
